SQLiteは軽量で扱いやすい一方、アプリが成長するとDBアクセスが散らばり、保守が難しくなるという問題があります。 そこで役立つのが Repositoryパターン + Unit of Work(UoW)。 この2つを組み合わせることで、DBアクセスの一元化・テスト容易性・将来のDB移行が圧倒的に楽になります。
この記事でわかること
・Repositoryパターンの役割
・Unit of Workでトランザクションを統合する方法
・SQLite × C# の実装例(非同期対応)
・将来のDB移行に強いアーキテクチャ
・業務アプリ向けベストプラクティス
・Repositoryパターンの役割
・Unit of Workでトランザクションを統合する方法
・SQLite × C# の実装例(非同期対応)
・将来のDB移行に強いアーキテクチャ
・業務アプリ向けベストプラクティス
1. Repositoryパターンとは?
Repositoryは、DBアクセスを抽象化する層です。 UIやViewModelはDBの種類を意識せず、Repositoryを通してデータを扱います。
■ 役割
- SQLをUI層に漏らさない
- DB変更(SQLite → SQL Server)に強い
- テストがしやすい(モック化可能)
2. Unit of Work(UoW)とは?
Unit of Workは、複数Repositoryの操作を1つのトランザクションにまとめる仕組みです。
■ 役割
- 複数テーブル更新をまとめてCommit
- 失敗時はRollback
- トランザクション境界を統一
重要:
SQLiteは同時書き込みに弱いため、UoWで書き込みを1つにまとめるのは非常に効果的。
3. サンプルドメイン(User + Order)
今回は以下の2テーブルを例にします。
Users(Id, Name)
Orders(Id, UserId, Amount)
4. Repositoryインターフェース
public interface IUserRepository
{
Task<User?> GetByIdAsync(int id);
Task<IEnumerable<User>> GetAllAsync();
Task AddAsync(User user);
Task UpdateAsync(User user);
Task DeleteAsync(int id);
}
public interface IOrderRepository
{
Task<IEnumerable<Order>> GetByUserIdAsync(int userId);
Task AddAsync(Order order);
}
5. Unit of Workインターフェース
public interface IUnitOfWork : IAsyncDisposable
{
IUserRepository Users { get; }
IOrderRepository Orders { get; }
Task CommitAsync();
Task RollbackAsync();
}
6. SQLite実装(Microsoft.Data.Sqlite)
■ Unit of Work実装
using Microsoft.Data.Sqlite;
public class SqliteUnitOfWork : IUnitOfWork
{
private readonly SqliteConnection _con;
private readonly SqliteTransaction _tran;
public IUserRepository Users { get; }
public IOrderRepository Orders { get; }
public SqliteUnitOfWork(string cs)
{
_con = new SqliteConnection(cs);
_con.Open();
_tran = _con.BeginTransaction();
Users = new SqliteUserRepository(_con, _tran);
Orders = new SqliteOrderRepository(_con, _tran);
}
public async Task CommitAsync() => await _tran.CommitAsync();
public async Task RollbackAsync() => await _tran.RollbackAsync();
public async ValueTask DisposeAsync()
{
await _tran.DisposeAsync();
await _con.DisposeAsync();
}
}
■ UserRepository実装
public class SqliteUserRepository : IUserRepository
{
private readonly SqliteConnection _con;
private readonly SqliteTransaction _tran;
public SqliteUserRepository(SqliteConnection con, SqliteTransaction tran)
{
_con = con;
_tran = tran;
}
public async Task<User?> GetByIdAsync(int id)
{
var cmd = _con.CreateCommand();
cmd.Transaction = _tran;
cmd.CommandText = "SELECT Id, Name FROM Users WHERE Id = @id";
cmd.Parameters.AddWithValue("@id", id);
using var reader = await cmd.ExecuteReaderAsync();
if (!await reader.ReadAsync()) return null;
return new User
{
Id = reader.GetInt32(0),
Name = reader.GetString(1)
};
}
public async Task AddAsync(User user)
{
var cmd = _con.CreateCommand();
cmd.Transaction = _tran;
cmd.CommandText = "INSERT INTO Users (Name) VALUES (@name)";
cmd.Parameters.AddWithValue("@name", user.Name);
await cmd.ExecuteNonQueryAsync();
}
public async Task UpdateAsync(User user)
{
var cmd = _con.CreateCommand();
cmd.Transaction = _tran;
cmd.CommandText = "UPDATE Users SET Name = @name WHERE Id = @id";
cmd.Parameters.AddWithValue("@name", user.Name);
cmd.Parameters.AddWithValue("@id", user.Id);
await cmd.ExecuteNonQueryAsync();
}
public async Task DeleteAsync(int id)
{
var cmd = _con.CreateCommand();
cmd.Transaction = _tran;
cmd.CommandText = "DELETE FROM Users WHERE Id = @id";
cmd.Parameters.AddWithValue("@id", id);
await cmd.ExecuteNonQueryAsync();
}
}
7. Unit of Workの利用例(アプリ側)
public async Task CreateUserWithOrder()
{
await using var uow = new SqliteUnitOfWork("Data Source=app.db");
var user = new User { Name = "Taro" };
await uow.Users.AddAsync(user);
var order = new Order { UserId = user.Id, Amount = 5000 };
await uow.Orders.AddAsync(order);
await uow.CommitAsync(); // 2つのINSERTが1トランザクションで確定
}
これにより、複数テーブル更新が一貫性を持って処理されます。
8. 将来のDB移行が圧倒的に楽になる理由
Repository + UoW を導入すると、 UI層・ViewModel・サービス層はDBを意識しなくなるため、 SQLite → SQL Server / PostgreSQL への移行が非常に簡単になります。
■ 移行時に変えるのはここだけ
- Unit of Work実装
- Repository実装
アプリ本体は一切変更不要。
9. 業務アプリ向けベストプラクティス
- Repositoryは「1テーブル1クラス」でシンプルに
- Unit of Workでトランザクション境界を統一
- 非同期(async/await)でUIフリーズを防ぐ
- SQLiteは同時書き込みに弱い → UoWで直列化
- 将来のDB移行を見据えてSQLをRepositoryに閉じ込める
まとめ:Repository + UoW は“長期運用アプリの必須アーキテクチャ”
- DBアクセスが整理され、保守性が大幅に向上
- 複数テーブル更新を安全に扱える
- SQLiteの弱点(同時書き込み)にも強くなる
- 将来のDB移行が圧倒的に楽になる
SQLite × C# の業務アプリを長く運用するなら、 Repository + Unit of Work は必須級のアーキテクチャです。 この記事をベースに、自分のプロジェクトに合わせて拡張してみてください。